package com.googlecode.pathmachine.nn;

/**
 *Name: NeuralNet
 *
 *Author: John Pendlebury ported to Java from code written by Matt Buckland
 *
 *Description: Represents a group of Neuron Layers
 *
 *You can reuse this code provided you include these comments.
 *I'd also appreciate you letting me know via email just because I'm
 *interested.
 *
 *email: john.pendlebury2@mail.dcu.ie
 */
import java.util.*;

public class NeuralNet {
    //Layers in the nerual net

    private ArrayList<NeuronLayer> layers;
    //The number of inputs to the neural net
    private int numInputs;
    //The number of outputs from the neural net
    private int numOutputs;
    //The number of hidden layers
    private int numHiddenLayers;
    //The number of neurons in a hidden layer
    private int neuronsPerHiddenLayer;
    //For debug purposes
    int chromosomelength;

    public NeuralNet() {
        //initialse member variables
        //Todo: Param class
        numInputs = Params.numInputs;
        numOutputs = Params.numOutputs;
        numHiddenLayers = Params.numHiddenLayers;
        neuronsPerHiddenLayer = Params.neuronsPerHiddenLayer;


        //Create the array of neuron layers the size of which equals the
        //number of hidden layers plus 1 for the output layer
        layers = new ArrayList<NeuronLayer>(numHiddenLayers + 1);

        if (numHiddenLayers > 0) {
            //Create the first hidden layer the number of inputs to
            //each neuron in this layer will be the number of
            //inputs to the NN itself.
            layers.add(new NeuronLayer(neuronsPerHiddenLayer, numInputs));

            //Create each subsequest hidden layer in which
            //the number of inputs to each neuron will equal the number of neurons 
            //in the previous hidden layer (i.e. neuronsPerHiddenLayer),
            //because each neuron has one output.
            for (int i = 1; i < numHiddenLayers; i++) {
                layers.add(new NeuronLayer(neuronsPerHiddenLayer, neuronsPerHiddenLayer));
            }

            //Create the output layer in which the number of inputs to each neuron
            //will equal the number of outputs in the previous hidden layer
            //and the number of outputs (i.e. the number of neurons) will
            //equal the number of outputs from the neural net itself.
            layers.add(new NeuronLayer(numOutputs, neuronsPerHiddenLayer));

            //Debug only
            chromosomelength = neuronsPerHiddenLayer * (numInputs + 1); //The first hidden layer
            chromosomelength += (numHiddenLayers - 1) * (neuronsPerHiddenLayer * (neuronsPerHiddenLayer + 1));
            chromosomelength += (numOutputs) * (neuronsPerHiddenLayer + 1);

        } else {
            //Create the one and only layer (the outout layer) in which the number of
            //inputs to each neuron will equal the number of inouts to the NN itself
            //and the number of neurons (i.e. the number of outputs) will
            //equal the number of outputs in the NN itself.
            layers.add(new NeuronLayer(numOutputs, numInputs));

            chromosomelength = numOutputs * (numInputs + 1);
        }

    }

    public List<Double> update(List<Double> inputs) {
        //An array to store outputs from each layer
        List<Double> outputs = null;

        //Check that we have received the correct number of inputs to the NN
        assert inputs.size() == numInputs;

        //For each layer including the output layer
        for (int i = 0; i < layers.size(); i++) {
            if (i > 0) {
                //If we're on a layer after the first layer
                //we take our inputs from the previous layer
                //and not the inputs to the NN
                inputs = outputs;
            }

            //Get the output from the current neuron layer
            outputs = layers.get(i).processInputs(inputs);
        }

        return outputs;
    }

    public int getChromosomeLength() {
        //We could just return chromosomeLength calculated in the constructor
        //but I want to try and add neurond to a existing NN at a later stage
        int chromosomeLength = 0;

        //For each layer including the output layer
        for (int i = 0; i < layers.size(); i++) {
            //Add the length of the chromosome segment for this layer
            chromosomeLength += layers.get(i).getGeneticEncodeLength();
        }

        //Check our calculations
        assert chromosomeLength == this.chromosomelength;

        return chromosomeLength;
    }

    public void setWeights(List<Double> genes) {
        //Check that we are getting the correct number of genes
        assert genes.size() == chromosomelength;

        int offset = 0;
        //For each layer including the output layer
        for (int i = 0; i < layers.size(); i++) {
            //Get the length of the chromosome segment for this layer
            int chromosomeLength = layers.get(i).getGeneticEncodeLength();

            List<Double> layerWeights = genes.subList(offset, offset + chromosomeLength);
            offset += chromosomeLength;
            layers.get(i).setWeights(layerWeights);
        }
    }
}
